package com.mtw.bbs.fileAdmin.service.impl;


import cn.hutool.core.io.FileUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.mtw.bbs.common.core.exception.BusinessException;
import com.mtw.bbs.common.redisBase.util.RedisUtilz;
import com.mtw.bbs.fileAdmin.constant.FileTypeEnum;
import com.mtw.bbs.fileAdmin.mapper.UploadPartMapper;
import com.mtw.bbs.fileAdmin.mapper.VideoInfoMapper;
import com.mtw.bbs.fileAdmin.pojo.entity.UploadPart;
import com.mtw.bbs.fileAdmin.pojo.entity.VideoInfo;
import com.mtw.bbs.fileAdmin.pojo.form.FileUploadForm;
import com.mtw.bbs.fileAdmin.pojo.entity.FileEntity;
import com.mtw.bbs.fileAdmin.pojo.entity.FileShard;
import com.mtw.bbs.fileAdmin.exception.FileBusException;
import com.mtw.bbs.fileAdmin.mapper.FileMapper;
import com.mtw.bbs.fileAdmin.pojo.vo.FileVo;
import com.mtw.bbs.fileAdmin.service.FileService;
import com.mtw.bbs.fileAdmin.service.FileShardService;
import com.mtw.bbs.fileAdmin.service.VideoInfoService;
import com.mtw.bbs.fileAdmin.strategy.UploadStrategy;
import com.mtw.bbs.fileAdmin.util.FileUtilz;
import com.mtw.bbs.fileAdmin.util.VideoUtil;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.concurrent.CompletableFuture;

import static com.mtw.bbs.fileAdmin.constant.FAConstants.*;


/**
 * 文件逻辑层
 */
@Slf4j
@Service("fileService")
@RequiredArgsConstructor
public class FileServiceImpl extends ServiceImpl<FileMapper, FileEntity> implements FileService {

    /**
     * 上传策略
     */
    @Value("${strategy.upload}")
    private String strategy;
    private final RedissonClient redissonClient;
    private final VideoInfoMapper videoInfoMapper;
    private final UploadPartMapper uploadPartMapper;
    private final FileShardService fileShardService;
    private final VideoInfoService videoInfoService;
    private final ThreadPoolTaskExecutor asyncExecutor;
    private final RedisUtilz<String,FileVo> redisUtilz;
    private final Collection<UploadStrategy> uploadStrategies;


    /**
     * 上传文件
     *
     * @param dto       文件信息
     * @return          保存后的文件信息
     */
    @SneakyThrows
    @Override
    public String uploadFile(FileUploadForm dto) {

        if (dto.getFile() == null) {
            throw new FileBusException("文件不存在");
        }

        String md5 = dto.getMd5();
        if (StringUtils.isBlank(md5)) {
            throw new FileBusException("文件md5为空");
        }

        LambdaQueryWrapper<FileEntity> lqw = new LambdaQueryWrapper<>();
        lqw.select(FileEntity::getId);
        lqw.eq(FileEntity::getMd5,md5);
        List<FileEntity> savedList = list(lqw);
        if (!CollectionUtils.isEmpty(savedList)){
            return savedList.get(0).getId();
        }

        String filename = dto.getFile().getOriginalFilename();
        String fileFormat = FileUtilz.getFileFormatByName(filename);


        Date date = new Date();
        // uuid重命名问价
        String newFileName = FileUtilz.renameByUUID(filename);
        // 文件路径，格式：分类文件夹 + 日期 + 文件名
        String path = FileUtilz.creatPath(newFileName, date);
        // 实体信息
        FileEntity entity = new FileEntity();
        entity.setCreatedDate(date);
        entity.setOriginName(filename);
        entity.setFileSuffix(fileFormat);
        entity.setFileType(FileUtilz.getBusinessFileTypeByFormat(fileFormat));
        entity.setMd5(dto.getMd5());
        entity.setName(newFileName);
        entity.setInnerPath(path);

        boolean result = false;
        for (UploadStrategy item : uploadStrategies) {
            if (item.strategy().equals(strategy)){
                result = item.upload(dto.getFile(), entity);
            }
        }

        if (!result) {
            throw new FileBusException("文件上传失败");
        }

        // 入库
        save(entity);
        log.info("文件上传，文件路径：{}，id：{}", path, entity.getId());

        // 如果是视频，设置视频信息
        if (FileUtilz.fileIsVideo(filename)){
            setVideoInfo(path,entity.getId());
        }

        return entity.getId();

    }


    /**
     * 上传文件
     *
     * @param dto       文件信息
     * @return          保存后的文件信息
     */
    @Override
    public void uploadFileByShard(FileUploadForm dto) {


        if (dto.getFile() == null) {
            throw new FileBusException("文件不存在");
        }

        String md5 = dto.getMd5();
        if (StringUtils.isBlank(md5)) {
            throw new FileBusException("文件md5不存在");
        }
        String filename = dto.getName();
        FileShard fileShard;
        // 获取分片信息
        RLock lock = redissonClient.getLock(REDIS_LOCK_KEY+dto.getMd5());
        lock.lock();
        try {
            fileShard = fileShardService.getOne(new LambdaQueryWrapper<FileShard>().eq(FileShard::getMd5, md5));
            if (fileShard == null) {
                Date date = new Date();
                String rename = FileUtilz.renameByUUID(filename);
                // 文件路径，格式：分类文件夹 + 日期 + 文件名
                String path = FileUtilz.creatPath(rename, date);

                fileShard = new FileShard();
                fileShard.setMd5(md5);
                fileShard.setShardCount(dto.getShardCount());
                fileShard.setFileSize(dto.getFileSize());
                fileShard.setOriginName(filename);
                fileShard.setName(rename);
                fileShard.setPath(path);
                // 文件路径，格式：分类文件夹 + 日期 + ( MD5_当前片 ) + 文件类型
                String tempPath = FileTypeEnum.SHARD + File.separator + FileUtilz.creatShardFilePath(fileShard.getMd5(), DateFormatUtils.format(date, "yyyyMMdd"));
                fileShard.setTempPath(tempPath);
                fileShardService.save(fileShard);
                log.info("分片文件信息保存成功，文件md5：{}，文件名称：{}，id：{}", md5, filename, fileShard.getId());
            }
        }catch (Exception e){
            throw new BusinessException(e.getMessage(),e);
        }finally {
            lock.unlock();
        }

        fileShard.setIndex(dto.getIndex());
        boolean result = false;
        for (UploadStrategy item : uploadStrategies) {
            if (item.strategy().equals(strategy)){
                result = item.shardUpload(dto.getFile(), fileShard);
            }
        }
        log.info("分片文件上传，结果：{}，文件路径：{}，id：{}", result, fileShard.getTempPath(), fileShard.getId());
        if (!result) {
            throw new FileBusException("文件上传失败");
        }

    }




    /**
     * 分片块检查
     * @param md5       文件md5
     * @return          上传到第几块
     */
    @Override
    public Integer shardCheck(String md5) {
        FileShard fileShard = fileShardService.getOne(new LambdaQueryWrapper<FileShard>().eq(FileShard::getMd5, md5));
        if (fileShard == null){
            return null;
        }
        LambdaQueryWrapper<UploadPart> lqw = new LambdaQueryWrapper<>();
        lqw.select(UploadPart::getPartNumber);
        lqw.eq(UploadPart::getShareId,fileShard.getId());
        List<UploadPart> uploadParts = uploadPartMapper.selectList(lqw);
        if (CollectionUtils.isEmpty(uploadParts)){
            return null;
        }
        List<Integer> list = uploadParts.stream().map(UploadPart::getPartNumber).sorted().toList();
        for (int i = 0; i < list.size()-1; i++) {
            Integer y = list.get(i);
            Integer n = list.get(i+1);
            if (y+1 != n){
                return y;
            }
        }
        return list.get(list.size()-1);
    }


    /**
     * 合并文件
     * @param dto       文件信息
     * @return          保存后的文件信息
     */
    @Transactional
    @SneakyThrows
    @Override
    public FileVo mergeFile(FileUploadForm dto) {

        // 获取分片信息
        FileShard fileShard = fileShardService.getOne(new LambdaQueryWrapper<FileShard>().eq(FileShard::getMd5, dto.getMd5()));
        if (fileShard == null) {
            throw new FileBusException("文件分片信息不存在");
        }

        // 查询片数据
        if (!fileShard.getShardCount().equals(dto.getShardCount())) {
            throw new FileBusException("文件分片信息错误");
        }

        // 实体信息
        FileEntity entity = new FileEntity();
        BeanUtils.copyProperties(fileShard,entity);
        entity.setFileSuffix( FileUtilz.getFileFormatByName(fileShard.getName()));
        entity.setFileType(FileUtilz.getBusinessFileTypeByName(fileShard.getName()));
        entity.setName(fileShard.getName());
        entity.setInnerPath(fileShard.getPath());


        boolean result = false;
        for (UploadStrategy item : uploadStrategies) {
            if (item.strategy().equals(strategy)){
                result = item.mergeFile(fileShard);
            }
        }

        if (!result) {
            throw new FileBusException("文件合并失败");
        }
        // 保存文件信息
        save(entity);
        log.info("分片文件合并成功，MD5：{}，新文件路径 {}", fileShard.getMd5(),fileShard.getPath());


        // 删除分片信息
        fileShardService.removeById(fileShard.getId());
        uploadPartMapper.delete(new LambdaQueryWrapper<UploadPart>().eq(UploadPart::getShareId,fileShard.getId()));

        FileVo vo = new FileVo();
        BeanUtils.copyProperties(entity,vo);

        // 如果是视频，设置视频信息
        if (FileUtilz.fileIsVideo(fileShard.getName())){
            log.info("文件：{},是视频，设置视频信息",fileShard.getName());
            setVideoInfo(fileShard.getPath(),entity.getId());
        }

        return vo;
    }


    // 设置视频信息
    private void setVideoInfo(String filePath,String fileId){

        CompletableFuture.runAsync(()->{
            log.info("开始生成封面");
            InputStream inputStream = null;
            for (UploadStrategy item : uploadStrategies) {
                if (item.strategy().equals(strategy)){
                    inputStream = item.getFileStream(filePath);
                }
            }
            if (inputStream == null){
                log.error("视频不存在：path：{}",filePath);
                return;
            }

            File tempFile = FileUtil.createTempFile(UUID.randomUUID()+"1", "jpg", new File(FileUtilz.CLASS_PATH + File.separator + FileTypeEnum.VIDEO_COVER), true);
            String coverPath =  filePath.substring(0, filePath.lastIndexOf("."))+".jpg";
            coverPath = coverPath.replace(FileTypeEnum.VIDEO,FileTypeEnum.VIDEO_COVER);

            Long duration = VideoUtil.getVideoCoverAndDuration(inputStream, tempFile);
            log.info("封面生成成功：path：{}",filePath);
            // 上传封面
            for (UploadStrategy item : uploadStrategies) {
                if (item.strategy().equals(strategy)){
                    item.simpleUpload(tempFile,coverPath);
                }
            }

            VideoInfo info = new VideoInfo();
            info.setId(fileId);
            info.setCoverPath(coverPath);
            info.setVideoDuration(duration);
            videoInfoMapper.insert(info);
            log.info("保存封面信息");
            FileUtil.del(tempFile);

        }, asyncExecutor);
    }


    /**
     * 获取文件url
     */
    @Override
    public FileVo getFileUrl(String id) {

        if (StringUtils.isBlank(id)){
            return new FileVo();
        }

        FileVo vo4R = redisUtilz.get(FILE_URL_REDIS_KEY + id);
        if (vo4R != null){
            return vo4R.getUrl() != null?vo4R:null;
        }

        FileEntity file = getById(id);
        if (file == null){
            redisUtilz.setForTimeMIN(FILE_URL_REDIS_KEY + id,new FileVo(),30);
            return null;
        }
        FileVo vo = new FileVo();
        vo.setId(id);
        vo.setName(file.getName());
        vo.setFileSuffix(file.getFileSuffix());

        // 如果是视频，设置视频信息
        if (FileUtilz.fileIsVideo(file.getName())){
            vo.setVideo(videoInfoService.getInfoByI(file.getId()));
        }

        for (UploadStrategy item : uploadStrategies) {
            if (item.strategy().equals(strategy)){
                String fileUrl = item.getFileUrl(file.getInnerPath());
                vo.setUrl(fileUrl);
                redisUtilz.setForTimeMIN(FILE_URL_REDIS_KEY + id,vo,23*60);
            }
        }
        return vo;
    }


}
